iT邦幫忙

2024 iThome 鐵人賽

DAY 14
0
自我挑戰組

Linux Kernel 網路巡禮系列 第 14

網路命名空間、proc檔案系統與nsfs檔案系統 (2)

  • 分享至 

  • xImage
  •  

昨天我們探討了 setns 的整個實作流程,並發現 process 123 使用的 network namespace 結構 (net) 的 ns_common 會保存在當我們打開 /proc/123/ns/net 時,透過 file->f_inode->i_private 取得。我們今天就要進一步了解,這個關聯是如何建立的。

https://ithelp.ithome.com.tw/upload/images/20240928/20152703S6shSeyTgz.png

/proc/<pid>/ns 目錄的生成機制

首先,我們需要檢視 proc 檔案系統是如何生成 /proc/<pid>/ns 目錄的。

// proc/base.c
static const struct pid_entry tgid_base_stuff[] = {
	DIR("task",       S_IRUGO|S_IXUGO, proc_task_inode_operations, proc_task_operations),
	DIR("fd",         S_IRUSR|S_IXUSR, proc_fd_inode_operations, proc_fd_operations),
	DIR("map_files",  S_IRUSR|S_IXUSR, proc_map_files_inode_operations, proc_map_files_operations),
	DIR("fdinfo",     S_IRUGO|S_IXUGO, proc_fdinfo_inode_operations, proc_fdinfo_operations),
	DIR("ns",	  S_IRUSR|S_IXUGO, proc_ns_dir_inode_operations, proc_ns_dir_operations),
#ifdef CONFIG_NET
	DIR("net",        S_IRUGO|S_IXUGO, proc_net_inode_operations, proc_net_operations),
#endif
    REG("comm",      S_IRUGO|S_IWUSR, proc_pid_set_comm_operations),
    ...

可以看到,ns 是一個目錄,且其 inode_operationsproc_ns_dir_inode_operationsfile_operations 則是 proc_ns_dir_operations

因此,當我們打開 /proc/123/ns/net 時,會呼叫 proc_ns_dir_inode_operations.lookup 來建立/proc/123/ns/net 的 dentry 和 inode。

// fs/proc/namespaces.c
static struct dentry *proc_ns_dir_lookup(struct inode *dir,
				struct dentry *dentry, unsigned int flags)
{
	struct task_struct *task = get_proc_task(dir); // 取得 process 123 的 task_struct
	const struct proc_ns_operations **entry, **last;
	unsigned int len = dentry->d_name.len;
	struct dentry *res = ERR_PTR(-ENOENT);

	last = &ns_entries[ARRAY_SIZE(ns_entries)];
	for (entry = ns_entries; entry < last; entry++) {
		if (strlen((*entry)->name) != len)
			continue;
		if (!memcmp(dentry->d_name.name, (*entry)->name, len)) // 找到 name 為 net 的 entry
			break;
	}
	if (entry == last)
		goto out;

	res = proc_ns_instantiate(dentry, task, *entry); // 執行 proc_ns_instantiate
out:
	put_task_struct(task);
out_no_task:
	return res;
}

這段 lookup 函數相當複雜,我們可以分段解析:

  1. 輸入包含 /proc/123/ns 的 inode 和 /proc/123/ns/net 的空 dentry。
  2. 透過 get_proc_task 取得 process 123 的 task_struct,並將其保存到 task
  3. 嘗試從 ns_entries 陣列中找到一個名稱為 net 的 entry。
  4. /proc/123/ns/net 的空 dentry、task_struct 和找到的 entry 傳遞給 proc_ns_instantiate 處理。
static const struct proc_ns_operations *ns_entries[] = {
#ifdef CONFIG_NET_NS
	&netns_operations, // network namespace
#endif
#ifdef CONFIG_UTS_NS
	&utsns_operations,
#endif
#ifdef CONFIG_IPC_NS
	&ipcns_operations,
#endif
#ifdef CONFIG_PID_NS
	&pidns_operations,
	&pidns_for_children_operations,
#endif
#ifdef CONFIG_USER_NS
	&userns_operations,
#endif
	&mntns_operations,
#ifdef CONFIG_CGROUPS
	&cgroupns_operations,
#endif
#ifdef CONFIG_TIME_NS
	&timens_operations,
	&timens_for_children_operations,
#endif
};

這裡的 entry 就是昨天介紹過的 proc_ns_operations,而 netns_operations 則是專門用於處理 network namespace 的操作結構。

// net/core/net_namespace.c
const struct proc_ns_operations netns_operations = {
	.name		= "net",
	.type		= CLONE_NEWNET,
	.get		= netns_get,
	.put		= netns_put,
	.install	= netns_install,
	.owner		= netns_owner,
};

其中 netns_operations,就是昨天介紹由 network namespace 系統定義的,當然他的 name 欄位就對應到 net

回到 proc_ns_dir_lookup,最終呼叫 proc_ns_instantiate,將 /proc/123/ns/net 的 dentry、process 123 和 netns_operations 傳入進行進一步處理。

// fs/proc/namespaces.c
static struct dentry *proc_ns_instantiate(struct dentry *dentry,
	struct task_struct *task, const void *ptr)
{
	const struct proc_ns_operations *ns_ops = ptr; // netns_operations
	struct inode *inode;
	struct proc_inode *ei;

	inode = proc_pid_make_inode(dentry->d_sb, task, S_IFLNK | S_IRWXUGO); // 軟連結
	if (!inode)
		return ERR_PTR(-ENOENT);

	ei = PROC_I(inode);
	inode->i_op = &proc_ns_link_inode_operations; // 設置 inode_operations
	ei->ns_ops = ns_ops;
	pid_update_inode(task, inode);

	d_set_d_op(dentry, &pid_dentry_operations);
	return d_splice_alias(inode, dentry);
}


proc_ns_instantiate 實現的是 lookup 函數的邏輯,我們可以關注幾個重點:

  1. 該檔案是一個 S_IFLNK(symbolic link)軟連結檔案。
  2. inode 的 i_op (inode_operations) 是 proc_ns_link_inode_operations
  3. ns_ops(即 netns_operations)會被保存到 ei->ns_ops
struct proc_inode {
	struct pid *pid;
	unsigned int fd;
	union proc_op op;
	struct proc_dir_entry *pde;
	struct ctl_table_header *sysctl;
	struct ctl_table *sysctl_entry;
	struct hlist_node sibling_inodes;
	const struct proc_ns_operations *ns_ops;
	struct inode vfs_inode;
} __randomize_layout;

proc_inode 是 proc 設計的一個擴充的 inode 結構,用來保存 proc 檔案系統需要的 process 資訊 (task_struct) 等,也將 VFS 的 inode 結構內嵌在 proc_inode 裡面。當透過 proc_pid_make_inode 申請一個 inode 時,實際上建立的是 proc_inode 實例,然後返回子結構 vfs_inode。所以 PROC_I macro 可以透過 container_of 拿到外層的 proc_inode 結構。

所以可以發現 /proc/123/ns/net是一個 symbolic link 檔案,對於軟連結檔案,你透過 open system call 打開時,最終 file 結構指向不是打開的那個檔案,而是連結的目標檔案。

Symbolic Link 與目標檔案的連結

在 Linux 的檔案系統中,symbolic link 是一種特殊的檔案類型,透過它可以連結到其他檔案。對於 symbolic link 檔案,相關的 inode 具有兩個重要的函數:readlinkget_link

static const struct inode_operations proc_ns_link_inode_operations = {
	.readlink	= proc_ns_readlink,
	.get_link	= proc_ns_get_link,
	.setattr	= proc_setattr,
};

int (*readlink) (struct dentry *, char __user *,int);
const char * (*get_link) (struct dentry *, struct inode *, struct delayed_call *);

readlink 的功能是取得該 symbolic link 所指向的檔案名稱。

> ls /proc/1/ns -l
total 0
lrwxrwxrwx 1 root root 0  九  19 22:02 cgroup -> 'cgroup:[4026531835]'
lrwxrwxrwx 1 root root 0  九  19 22:02 ipc -> 'ipc:[4026531839]'
lrwxrwxrwx 1 root root 0  九  19 22:02 mnt -> 'mnt:[4026531840]'
lrwxrwxrwx 1 root root 0  九  19 22:02 net -> 'net:[4026531992]'
lrwxrwxrwx 1 root root 0  九  19 22:02 pid -> 'pid:[4026531836]'
lrwxrwxrwx 1 root root 0  九  19 22:02 pid_for_children -> 'pid:[4026531836]'
lrwxrwxrwx 1 root root 0  九  19 22:02 user -> 'user:[4026531837]'
lrwxrwxrwx 1 root root 0  九  19 22:02 uts -> 'uts:[4026531838]'

ls 指令看到的 net:[4026531992] ,就致 net 這個 symbolic link 檔案連結過去的檔案名稱。

接下來,我們來探討 /proc/123/ns/net inode 使用的 proc_ns_readlink 函數是如何取得 symbolic link 的目標檔案名稱。

static int proc_ns_readlink(struct dentry *dentry, char __user *buffer, int buflen)
{
	struct inode *inode = d_inode(dentry);
	const struct proc_ns_operations *ns_ops = PROC_I(inode)->ns_ops;
	struct task_struct *task;
	char name[50];
	int res = -EACCES;

	task = get_proc_task(inode); // 從 inode 拿到 proc_inode,然後取得 process 123 的 task_struct
    ...
    res = ns_get_name(name, sizeof(name), task, ns_ops);
    ...
}

在這段程式碼中,proc_ns_readlink 函數首先從 inode 取得與該 inode 關聯的 task_struct,並呼叫 ns_get_name 函數來取得目標檔案的名稱。ns_get_name 函數的定義位於 fs/nsfs.c

終於我們要進入到今天的另外一個主角 NameSpace File System (NSFS)。這個是 Linux 專門針對 /proc/123/ns/net 這類指向 namespace 結構的檔案專門設計的檔案系統。

NameSpace File System (NSFS)

特別注意,我使用的 kernel版本是 6.6.47,從 6.6.47 到最新的 6.11 中間,nsfs 檔案系統的程式碼有大幅度修改,函數名稱和實作內容都有蠻大的變化。

NSFS 是一種特殊的虛擬檔案系統,具有以下幾個特性:

  1. 每個 NSFS 檔案系統下的檔案都代表一個 namespace 實例,inodei_private 會指向該 namespace 實例的 ns_common 結構體。
  2. NSFS 檔案系統中不存在目錄結構,只有檔案,所有檔案都是根目錄檔案,直接掛載在 superblock 上。
  3. 系統啟動時,內核會掛載一個虛擬的 NSFS 檔案系統,用來存放 /proc/<pid>/ns/* 軟連結對應的檔案。這個掛起來的檔案系統,只是在 kernel 有對應的 vfsmount 掛載資料結構存在,但沒有掛載到 VFS 檔案樹上面,所以並沒有路徑可以直接找到這個預設的 NSFS 檔案系統,mount 指令也看不到。
  4. NSFS 檔案的名稱格式為 <namespace type>:<inode number>,使用 proc_ns_operations.name 作為 namespace type,然後使用 ns_common 結構的 inum 作為 inode number
  5. 這解釋了為何執行 ls /proc/123/ns/net 會顯示檔名 net:[4026531992]

下圖說明了打開 /proc/123/ns/net後, NSFS 的運作方式:

https://ithelp.ithome.com.tw/upload/images/20240928/20152703h8WL1x6YtX.png

在這張圖中,我們可以看到 /proc/123/ns/net 是一個 symbolic link 檔案,透過 open 系統呼叫會 "follow" 到 net:[4026531992],並取得對應的 ns_common 結構。

前面說到 proc_ns_readlink 會呼叫 ns_get_name 函數來取得 NSFS 檔案的名稱:

// fs/nsfs.c
int ns_get_name(char *buf, size_t size, struct task_struct *task,
			const struct proc_ns_operations *ns_ops) // 傳入 netns_operations
{
	struct ns_common *ns;
	int res = -ENOENT;
	const char *name;
	ns = ns_ops->get(task); // proc_ns_operations GET API
	if (ns) {
		name = ns_ops->real_ns_name ? : ns_ops->name;
		res = snprintf(buf, size, "%s:[%u]", name, ns->inum); // net:[4026531992]
		ns_ops->put(ns);
	}
	return res;
}

從這段程式碼可以看出,NSFS 的檔案名稱是動態產生的,而非從某個 dentry 讀取出來的。這裡使用 ns_ops.name 取得 namespace 的類型名稱 (例如 net),並使用 ns_ops->get 取得對應的 ns_common 結構,最終將 ns->inum 作為 inode number。

我們可以從這裡發現這真的是一個純虛擬的檔案系統,他的檔案名稱直接是造出來的,而不是從某個 dentry 讀出來的,這邊會透過輸入的 ns_ops.name 拿到 namespace 的類型名稱 (net),然後透過其面介紹的ns_ops->get 拿到對應namepsace類型的ns_common結構,這邊是使用net的ns_ops,所以就會拿到 network namespace的ns_common,然後就真的用 ns->inum當作inode number了。

總結

透過前面的探討,我們瞭解到,當我們打開 /proc/123/ns/net 時,/proc/123/ns/net 會軟連結到 NSFS 檔案系統中,代表 prcess 123 使用的 network namespace 的檔案。明天,我們將進一步探討打開 /proc/123/ns/net 時,系統是如何將 file 結構連結到 NSFS 檔案的,並瞭解 NSFS 檔案的 dentry 與 inode 是如何產生的。


上一篇
網路命名空間、proc檔案系統與nsfs檔案系統 (1)
系列文
Linux Kernel 網路巡禮14
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言